package gateway


import (
	"context"
	"crypto/tls"
	"encoding/json"
	"fmt"
	"gitee.com/wayjin/fabric-api/gateway/local"
	"gitee.com/wayjin/fabric-extension/cryptoutil"
	"github.com/golang/protobuf/proto"
	"github.com/hyperledger/fabric/core/chaincode/platforms"
	"github.com/hyperledger/fabric/core/chaincode/platforms/car"
	"github.com/hyperledger/fabric/core/chaincode/platforms/java"
	"github.com/hyperledger/fabric/core/chaincode/platforms/node"
	"github.com/hyperledger/fabric/core/chaincode/shim"
	"github.com/hyperledger/fabric/core/common/ccprovider"
	"github.com/hyperledger/fabric/core/container"
	"github.com/hyperledger/fabric/peer/common/api"
	"github.com/hyperledger/fabric/protos/common"
	"github.com/hyperledger/fabric/protos/peer"
	protopeer "github.com/hyperledger/fabric/protos/peer"
	protoutil "github.com/hyperledger/fabric/protos/utils"
	"github.com/pkg/errors"
	"strings"
	"time"
)

type ChaincodeSpec struct {
	Name          string
	Version       string
	Lang          string
	Args          string
	Path          string
	Policy        []byte
	CollectConfig []byte
	WaitForEvent  bool
}

var platformRegistry = platforms.NewRegistry(
	//&golang.Platform{},
	&car.Platform{},
	&java.Platform{},
	&node.Platform{},
	&local.Platform{},
)

func (spec ChaincodeSpec) getPeerChaincodeSpec() (*peer.ChaincodeSpec, error) {
	input := &peer.ChaincodeInput{}
	err := json.Unmarshal([]byte(spec.Args), &input)
	if err != nil {
		return nil, errors.Wrap(err, "chaincode argument error")
	}
	ccSpec := &peer.ChaincodeSpec{
		Type: peer.ChaincodeSpec_Type(peer.ChaincodeSpec_Type_value[strings.ToUpper(spec.Lang)]),
		ChaincodeId: &peer.ChaincodeID{
			Name:    spec.Name,
			Version: spec.Version,
			Path:    spec.Path,
		},
		Input: input,
	}
	return ccSpec, nil
}

func (spec ChaincodeSpec) getChaincodeDeploymentSpec(ccpkg bool) (*peer.ChaincodeDeploymentSpec, error) {
	var ccPackageBytes []byte
	ccSpec, err := spec.getPeerChaincodeSpec()
	if err != nil {
		return nil, err
	}
	if ccpkg {
		var err error
		ccPackageBytes, err = container.GetChaincodePackageBytes(platformRegistry, ccSpec)
		if err != nil {
			return nil, fmt.Errorf("fail to get chaincode package bytes: %v", err)
		}
	}
	return &peer.ChaincodeDeploymentSpec{ChaincodeSpec: ccSpec, CodePackage: ccPackageBytes}, nil
}

// install chaincode
func installChaincode(suite cryptoutil.CryptoSuite, peerCli *peerClient, spec ChaincodeSpec) error {
	// check exist
	if existed, _ := ccprovider.ChaincodePackageExists(spec.Name, spec.Version); existed {
		return fmt.Errorf("chaincode %s:%s already exists", spec.Name, spec.Version)
	}
	//
	deploySpec, err := spec.getChaincodeDeploymentSpec(true)
	if err != nil {
		return fmt.Errorf("fail to get chaincode deployment spec:%v", err)
	}

	signer, err := suite.NewSigner()
	if err != nil {
		return err
	}

	creator, err := signer.Serialize()
	if err != nil {
		return err
	}

	prop, _, err := protoutil.CreateInstallProposalFromCDS(deploySpec, creator)
	if err != nil {
		return fmt.Errorf("CreateInstallProposalFromCDS:%v", err)
	}

	signedProp, err := cryptoutil.GetSignedProposal(prop, signer)
	if err != nil {
		return fmt.Errorf("GetSignedProposal:%v", err)
	}

	endorser, err := peerCli.Endorser()
	if err != nil {
		return fmt.Errorf("GetPeerClientEndorser:%v", err)
	}

	resp, err := endorser.ProcessProposal(context.Background(), signedProp)
	if err != nil {
		return fmt.Errorf("ProcessProposal:%v", err)
	}

	if resp != nil {
		if resp.Response != nil && resp.Response.Status != 200 {
			return fmt.Errorf("invaid response status:%v", resp.Response.Status)
		}
		fmt.Printf("Successfully install chaincode:%v", resp.Response.Status)
	} else {
		return fmt.Errorf("reponse error: nil response")
	}
	return nil
}

var (
	escc = []byte("escc")
	vscc = []byte("vscc")
)

func instantiateChaincode(suite cryptoutil.CryptoSuite, spec ChaincodeSpec, ordererCli *ordererClient, peerCli *peerClient, channelID string) error {
	signer, err := suite.NewSigner()
	if err != nil {
		return err
	}

	creator, err := signer.Serialize()
	if err != nil {
		return err
	}

	deploySpec, err := spec.getChaincodeDeploymentSpec(false)
	prop, _, err := protoutil.CreateDeployProposalFromCDS(channelID, deploySpec, creator, spec.Policy, escc, vscc, spec.CollectConfig)
	if err != nil {
		return nil
	}
	signedProp, err := cryptoutil.GetSignedProposal(prop, signer)
	if err != nil {
		return fmt.Errorf("GetSignedProposal: %v", err)
	}
	endorser, err := peerCli.Endorser()
	if err != nil {
		return fmt.Errorf("GetEndorserClient:%v", err)
	}
	resp, err := endorser.ProcessProposal(context.Background(), signedProp)
	if err != nil {
		return fmt.Errorf("fail to process proposal:%v", err)
	}
	if resp != nil {
		if resp.Response != nil && resp.Response.Status != 200 {
			return fmt.Errorf("fail to proposal instantiated chaincode:%v", resp.Response.Message)
		}
		env, err := createSignedTx(prop, signer, resp)
		if err != nil {
			return fmt.Errorf("CreateSignedTx: %v", err)
		}
		if env != nil {
			broadcast, err := ordererCli.GetBroadcast()
			if err != nil {
				return fmt.Errorf("GetBroadcastClient:%v", err)
			}
			err = broadcast.Send(env)
			if err != nil {
				return fmt.Errorf("BroadcastClient send fail:%v", err)
			}
			fmt.Println("Successfully instantiate chaincode:", resp.Response.Status)
		}
	} else {
		fmt.Println("fail to proposal instantiate transaction")
	}
	return nil
}

//
func upgradeChaincode(suite cryptoutil.CryptoSuite, peerCli *peerClient, ordererCli *ordererClient, spec ChaincodeSpec, channelID string) error {
	// TODO
	deploySpec, err := spec.getChaincodeDeploymentSpec(false)
	if err != nil {
		return err
	}
	signer, err := suite.NewSigner()
	creator, err := signer.Serialize()
	prop, _, err := protoutil.CreateUpgradeProposalFromCDS(channelID, deploySpec, creator, spec.Policy, []byte(escc), []byte(vscc), spec.CollectConfig)
	if err != nil {
		return fmt.Errorf("error creating proposal %s", err)
	}

	signedProp, err := cryptoutil.GetSignedProposal(prop, signer)
	if err != nil {
		return fmt.Errorf("fail to get signed proposal:%v", err)
	}

	endorser, err := peerCli.Endorser()
	if err != nil {
		return fmt.Errorf("fail to get endorser:%v", err)
	}
	response, err := endorser.ProcessProposal(context.Background(), signedProp)
	if err != nil {
		return fmt.Errorf("proposal chaincode upgrade fail: %v", err)
	}

	if response != nil {
		env, err := createSignedTx(prop, signer, response)
		if err != nil {
			return fmt.Errorf("fail to create signed transaction:%v", err)
		}

		broadcast, err := ordererCli.GetBroadcast()
		if err != nil {
			return fmt.Errorf("fail to get broadcasr:%v", err)
		}
		err = broadcast.Send(env)
		broadcast.Close()
		return err
	}
	return nil
}

type ChaincodeInvokeSpec struct {
	Spec              ChaincodeSpec
	Invoke            bool
	TxID              string
	CID               string
	ChannelID         string
	Transient         string
	WaitForEvent      bool
	WaitPeerAddresses []string
}

func invokeOrQueryChainCode(suite cryptoutil.CryptoSuite, invokeSpec ChaincodeInvokeSpec, cert tls.Certificate, endorsers []protopeer.EndorserClient, delievers []api.PeerDeliverClient, broadcast BroadcastClient) (*protopeer.ProposalResponse, error) {
	ccSpec, _ := invokeSpec.Spec.getPeerChaincodeSpec()
	invocation := &protopeer.ChaincodeInvocationSpec{ChaincodeSpec: ccSpec}
	signer, err := suite.NewSigner()
	if err != nil {
		return nil, err
	}
	creator, err := signer.Serialize()
	if err != nil {
		return nil, err
	}

	funcName := "invoke"
	if !invokeSpec.Invoke {
		funcName = "query"
	}
	// extract the transient field if it exists
	var tMap map[string][]byte
	if invokeSpec.Transient != "" {
		if err := json.Unmarshal([]byte(invokeSpec.Transient), &tMap); err != nil {
			return nil, errors.Wrap(err, "error parsing transient string")
		}
	}
	prop, txid, err := protoutil.CreateChaincodeProposalWithTxIDAndTransient(common.HeaderType_ENDORSER_TRANSACTION, invokeSpec.ChannelID, invocation, creator, invokeSpec.TxID, tMap)
	if err != nil {
		return nil, errors.WithMessage(err, fmt.Sprintf("error creating proposal for %s", funcName))
	}

	signedProp, err := cryptoutil.GetSignedProposal(prop, signer)
	if err != nil {
		return nil, errors.WithMessage(err, fmt.Sprintf("error creating signed proposal for %s", funcName))
	}

	var responses []*protopeer.ProposalResponse
	for _, endorser := range endorsers {
		resp, err := endorser.ProcessProposal(context.Background(), signedProp)
		if err != nil {
			return nil, errors.WithMessage(err, fmt.Sprintf("error endorsing %s", funcName))
		}
		responses = append(responses, resp)
	}

	if len(responses) == 0 {
		// this should only happen if some new code has introduced a bug
		return nil, errors.New("no proposal responses received - this might indicate a bug")
	}
	invokeSpec.WaitPeerAddresses = append(invokeSpec.WaitPeerAddresses, "")
	proposalResp := responses[0]
	if invokeSpec.Invoke {
		// TODO
		if proposalResp != nil {
			if proposalResp.Response.Status >= shim.ERRORTHRESHOLD {
				fmt.Println("response status not ok:", proposalResp.Response.Status)
				return proposalResp, nil
			}

			env, err := createSignedTx(prop, signer, responses...)
			if err != nil {
				return proposalResp, errors.WithMessage(err, "could not assemble transaction")
			}
			var (
				dg  *deliverGroup
				ctx context.Context
			)
			if invokeSpec.WaitForEvent {
				var cancelFunc context.CancelFunc
				ctx, cancelFunc = context.WithTimeout(context.Background(), 10*time.Second)
				defer cancelFunc()
				// fmt.Println("deliver size: ", len(delievers))
				dg = newDeliverGroup(delievers, invokeSpec.WaitPeerAddresses, cert, signer, invokeSpec.ChannelID, txid)
				// connect to deliver service on all peers
				err := dg.Connect(ctx)
				if err != nil {
					return nil, fmt.Errorf("connect deliever group fail:%v", err)
				}
			}
			// send the envelope for ordering
			if err = broadcast.Send(env); err != nil {
				return proposalResp, errors.WithMessage(err, fmt.Sprintf("error sending transaction for %s", funcName))
			}

			if dg != nil && ctx != nil {
				// wait for event that contains the txid from all peers
				err = dg.Wait(ctx)
				if err != nil {
					return nil, err
				}
			}
		}

	}

	return proposalResp, nil
}

func invokeChaincode(suite cryptoutil.CryptoSuite, ccSpec ChaincodeSpec, peerCli *peerClient, ordererCli *ordererClient, channelID string) (*ChainCodeInvokeResponse, error) {
	invokeSpec := ChaincodeInvokeSpec{
		Spec:         ccSpec,
		Invoke:       true,
		TxID:         "",
		ChannelID:    channelID,
		Transient:    "",
		WaitForEvent: ccSpec.WaitForEvent,
	}
	var (
		endorsers []peer.EndorserClient
		delivers  []api.PeerDeliverClient
	)
	endorser, _ := peerCli.Endorser()
	endorsers = append(endorsers, endorser)
	deliverCli, _ := peerCli.PeerDeliver()
	delivers = append(delivers, deliverCli)
	broadcast, _ := ordererCli.GetBroadcast()
	proposalResp, err := invokeOrQueryChainCode(suite, invokeSpec, peerCli.Certificate(), endorsers, delivers, broadcast)
	if err != nil {
		return nil, fmt.Errorf("fail to invoke chaincode: %v", err)
	}

	respPayload, err := protoutil.GetProposalResponsePayload(proposalResp.Payload)
	if err != nil {
		return nil, errors.WithMessage(err, "error while unmarshaling proposal response payload")
	}

	ca, err := protoutil.GetChaincodeAction(respPayload.Extension)
	if err != nil {
		return nil, errors.WithMessage(err, "error while unmarshaling chaincode action")
	}

	if proposalResp.Endorsement == nil {
		return nil, errors.Errorf("endorsement failure during invoke. response: %v", proposalResp.Response)
	}

	resp := &ChainCodeInvokeResponse{
		TxID:"",
		ProposalHash:respPayload.ProposalHash,
		Result:ca.Results,
		TxTime:proposalResp.Timestamp.String(),
	}

	fmt.Printf("proposal hash: %x\n", respPayload.ProposalHash)
	fmt.Printf("result: %x\n", ca.Results)
	fmt.Printf("Chaincode invoke successful. result: %v\n", ca.Response)
	return resp, nil
}

func queryChainCode(suite cryptoutil.CryptoSuite, ccSpec ChaincodeSpec, peerCli *peerClient, ordererCli *ordererClient, channelID string) (*ChainCodeQueryResponse, error) {
	invokeSpec := ChaincodeInvokeSpec{
		Spec:         ccSpec,
		Invoke:       false,
		TxID:         "",
		ChannelID:    channelID,
		Transient:    "",
		WaitForEvent: false,
	}
	var (
		endorsers []peer.EndorserClient
		delivers  []api.PeerDeliverClient
	)
	endorser, _ := peerCli.Endorser()
	endorsers = append(endorsers, endorser)
	broadcast, _ := ordererCli.GetBroadcast()
	proposalResp, err := invokeOrQueryChainCode(suite, invokeSpec, peerCli.Certificate(), endorsers, delivers, broadcast)
	if err != nil {
		return nil, fmt.Errorf("fail to invoke chaincode: %v", err)
	}

	if proposalResp == nil {
		return nil, errors.New("error during query: received nil proposal response")
	}
	if proposalResp.Endorsement == nil {
		return nil, errors.Errorf("endorsement failure during query. response: %v", proposalResp.Response)
	}

	ccResp := &ChainCodeQueryResponse{
		Payload: proposalResp.Response.Payload,
		Message: proposalResp.Response.Message,
		Status:  proposalResp.Response.Status,
	}
	//fmt.Println(string(proposalResp.Response.Payload))
	//fmt.Println("Query Raw: ", string(proposalResp.Response.Payload))
	//fmt.Printf("Query Hex: %x\n", proposalResp.Response.Payload)
	//fmt.Printf("endorsement: %x\n", proposalResp.Endorsement.Endorser)
	//fmt.Printf("Sign: %x\n", proposalResp.Endorsement.Endorser)
	return ccResp, nil
}

type ChaincodeInfo struct {
	Name    string
	Version string
	ID      string
}

func listChaincode(suite cryptoutil.CryptoSuite, peerCli *peerClient, installed, instantiated bool, channelID string) ([]ChaincodeInfo, error) {
	var prop *protopeer.Proposal
	var err error
	signer, err := suite.NewSigner()
	if err != nil {
		return nil, err
	}
	creator, err := signer.Serialize()
	if err != nil {
		return nil, err
	}

	if installed && !instantiated {
		prop, _, err = protoutil.CreateGetInstalledChaincodesProposal(creator)
	} else if instantiated {
		prop, _, err = protoutil.CreateGetChaincodesProposal(channelID, creator)
	}
	if prop == nil {
		return nil, fmt.Errorf("create proposal fail: proposal is nil")
	}
	if err != nil {
		return nil, fmt.Errorf("create proposal fail:%v", err)
	}

	signedProp, err := cryptoutil.GetSignedProposal(prop, signer)
	if err != nil {
		return nil, fmt.Errorf("create signed proposal fail:%v", err)
	}

	endorer, err := peerCli.Endorser()
	if err != nil {
		return nil, err
	}

	resp, err := endorer.ProcessProposal(context.Background(), signedProp)
	if err != nil {
		return nil, fmt.Errorf("process proposal fail:%v", err)
	}
	if resp.Response == nil {
		return nil, fmt.Errorf("nil response")
	}
	queryChaincode := protopeer.ChaincodeQueryResponse{}
	err = proto.Unmarshal(resp.Response.Payload, &queryChaincode)
	if err != nil {
		return nil, fmt.Errorf("proto unmarshal error: %v", err)
	}
	var chaincodes []ChaincodeInfo
	for _, cc := range queryChaincode.Chaincodes {
		//fmt.Println(cc.Name, cc.Version, cc.Id, cc.Path, cc.Escc, cc.Vscc)
		chaincodes = append(chaincodes, ChaincodeInfo{Name: cc.Name, Version: cc.Version, ID: fmt.Sprintf("%x", cc.Id)})
	}
	return chaincodes, nil
}

